/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.client;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureAdapter;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriTemplateHandler;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

/**
 * <strong>Spring's central class for asynchronous client-side HTTP access.</strong>
 * Exposes similar methods as {@link RestTemplate}, but returns {@link ListenableFuture}
 * wrappers as opposed to concrete results.
 *
 * <p>The {@code AsyncRestTemplate} exposes a synchronous {@link RestTemplate} via the
 * {@link #getRestOperations()} method and shares its {@linkplain #setErrorHandler error handler}
 * and {@linkplain #setMessageConverters message converters} with that {@code RestTemplate}.
 *
 * <p><strong>Note:</strong> by default {@code AsyncRestTemplate} relies on
 * standard JDK facilities to establish HTTP connections. You can switch to use
 * a different HTTP library such as Apache HttpComponents, Netty, and OkHttp by
 * using a constructor accepting an {@link org.springframework.http.client.AsyncClientHttpRequestFactory}.
 *
 * <p>For more information, please refer to the {@link RestTemplate} API documentation.
 *
 * @author Arjen Poutsma
 * @see RestTemplate
 * @since 4.0
 * @deprecated as of Spring 5.0, in favor of {@link org.springframework.web.reactive.function.client.WebClient}
 */
@Deprecated
public class AsyncRestTemplate extends org.springframework.http.client.support.InterceptingAsyncHttpAccessor
        implements AsyncRestOperations {

    private final RestTemplate syncTemplate;


    /**
     * Create a new instance of the {@code AsyncRestTemplate} using default settings.
     * <p>This constructor uses a {@link SimpleClientHttpRequestFactory} in combination
     * with a {@link SimpleAsyncTaskExecutor} for asynchronous execution.
     */
    public AsyncRestTemplate() {
        this(new SimpleAsyncTaskExecutor());
    }

    /**
     * Create a new instance of the {@code AsyncRestTemplate} using the given
     * {@link AsyncTaskExecutor}.
     * <p>This constructor uses a {@link SimpleClientHttpRequestFactory} in combination
     * with the given {@code AsyncTaskExecutor} for asynchronous execution.
     */
    public AsyncRestTemplate(AsyncListenableTaskExecutor taskExecutor) {
        Assert.notNull(taskExecutor, "AsyncTaskExecutor must not be null");
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setTaskExecutor(taskExecutor);
        this.syncTemplate = new RestTemplate(requestFactory);
        setAsyncRequestFactory(requestFactory);
    }

    /**
     * Create a new instance of the {@code AsyncRestTemplate} using the given
     * {@link org.springframework.http.client.AsyncClientHttpRequestFactory}.
     * <p>This constructor will cast the given asynchronous
     * {@code AsyncClientHttpRequestFactory} to a {@link ClientHttpRequestFactory}. Since
     * all implementations of {@code ClientHttpRequestFactory} provided in Spring also
     * implement {@code AsyncClientHttpRequestFactory}, this should not result in a
     * {@code ClassCastException}.
     */
    public AsyncRestTemplate(org.springframework.http.client.AsyncClientHttpRequestFactory asyncRequestFactory) {
        this(asyncRequestFactory, (ClientHttpRequestFactory) asyncRequestFactory);
    }

    /**
     * Creates a new instance of the {@code AsyncRestTemplate} using the given
     * asynchronous and synchronous request factories.
     *
     * @param asyncRequestFactory the asynchronous request factory
     * @param syncRequestFactory  the synchronous request factory
     */
    public AsyncRestTemplate(org.springframework.http.client.AsyncClientHttpRequestFactory asyncRequestFactory,
                             ClientHttpRequestFactory syncRequestFactory) {

        this(asyncRequestFactory, new RestTemplate(syncRequestFactory));
    }

    /**
     * Create a new instance of the {@code AsyncRestTemplate} using the given
     * {@link org.springframework.http.client.AsyncClientHttpRequestFactory} and synchronous {@link RestTemplate}.
     *
     * @param requestFactory the asynchronous request factory to use
     * @param restTemplate   the synchronous template to use
     */
    public AsyncRestTemplate(org.springframework.http.client.AsyncClientHttpRequestFactory requestFactory,
                             RestTemplate restTemplate) {

        Assert.notNull(restTemplate, "RestTemplate must not be null");
        this.syncTemplate = restTemplate;
        setAsyncRequestFactory(requestFactory);
    }

    private static ListenableFuture<URI> adaptToLocationHeader(ListenableFuture<HttpHeaders> future) {
        return new ListenableFutureAdapter<URI, HttpHeaders>(future) {
            @Override
            @Nullable
            protected URI adapt(HttpHeaders headers) throws ExecutionException {
                return headers.getLocation();
            }
        };
    }

    private static ListenableFuture<Set<HttpMethod>> adaptToAllowHeader(ListenableFuture<HttpHeaders> future) {
        return new ListenableFutureAdapter<Set<HttpMethod>, HttpHeaders>(future) {
            @Override
            protected Set<HttpMethod> adapt(HttpHeaders headers) throws ExecutionException {
                return headers.getAllow();
            }
        };
    }

    /**
     * Return the error handler.
     */
    public ResponseErrorHandler getErrorHandler() {
        return this.syncTemplate.getErrorHandler();
    }

    /**
     * Set the error handler.
     * <p>By default, AsyncRestTemplate uses a
     * {@link org.springframework.web.client.DefaultResponseErrorHandler}.
     */
    public void setErrorHandler(ResponseErrorHandler errorHandler) {
        this.syncTemplate.setErrorHandler(errorHandler);
    }

    /**
     * Configure default URI variable values. This is a shortcut for:
     * <pre class="code">
     * DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
     * handler.setDefaultUriVariables(...);
     *
     * AsyncRestTemplate restTemplate = new AsyncRestTemplate();
     * restTemplate.setUriTemplateHandler(handler);
     * </pre>
     *
     * @param defaultUriVariables the default URI variable values
     * @since 4.3
     */
    @SuppressWarnings("deprecation")
    public void setDefaultUriVariables(Map<String, ?> defaultUriVariables) {
        UriTemplateHandler handler = this.syncTemplate.getUriTemplateHandler();
        if (handler instanceof DefaultUriBuilderFactory) {
            ((DefaultUriBuilderFactory) handler).setDefaultUriVariables(defaultUriVariables);
        } else if (handler instanceof org.springframework.web.util.AbstractUriTemplateHandler) {
            ((org.springframework.web.util.AbstractUriTemplateHandler) handler)
                    .setDefaultUriVariables(defaultUriVariables);
        } else {
            throw new IllegalArgumentException(
                    "This property is not supported with the configured UriTemplateHandler.");
        }
    }

    /**
     * Return the configured URI template handler.
     */
    public UriTemplateHandler getUriTemplateHandler() {
        return this.syncTemplate.getUriTemplateHandler();
    }

    /**
     * This property has the same purpose as the corresponding property on the
     * {@code RestTemplate}. For more details see
     * {@link RestTemplate#setUriTemplateHandler}.
     *
     * @param handler the URI template handler to use
     */
    public void setUriTemplateHandler(UriTemplateHandler handler) {
        this.syncTemplate.setUriTemplateHandler(handler);
    }

    @Override
    public RestOperations getRestOperations() {
        return this.syncTemplate;
    }


    // GET

    /**
     * Return the message body converters.
     */
    public List<HttpMessageConverter<?>> getMessageConverters() {
        return this.syncTemplate.getMessageConverters();
    }

    /**
     * Set the message body converters to use.
     * <p>These converters are used to convert from and to HTTP requests and responses.
     */
    public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        this.syncTemplate.setMessageConverters(messageConverters);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> getForEntity(String url, Class<T> responseType, Object... uriVariables)
            throws RestClientException {

        AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
    }


    // HEAD

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> getForEntity(String url, Class<T> responseType,
                                                                Map<String, ?> uriVariables) throws RestClientException {

        AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> getForEntity(URI url, Class<T> responseType)
            throws RestClientException {

        AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.GET, requestCallback, responseExtractor);
    }

    @Override
    public ListenableFuture<HttpHeaders> headForHeaders(String url, Object... uriVariables)
            throws RestClientException {

        ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
        return execute(url, HttpMethod.HEAD, null, headersExtractor, uriVariables);
    }


    // POST

    @Override
    public ListenableFuture<HttpHeaders> headForHeaders(String url, Map<String, ?> uriVariables)
            throws RestClientException {

        ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
        return execute(url, HttpMethod.HEAD, null, headersExtractor, uriVariables);
    }

    @Override
    public ListenableFuture<HttpHeaders> headForHeaders(URI url) throws RestClientException {
        ResponseExtractor<HttpHeaders> headersExtractor = headersExtractor();
        return execute(url, HttpMethod.HEAD, null, headersExtractor);
    }

    @Override
    public ListenableFuture<URI> postForLocation(String url, @Nullable HttpEntity<?> request, Object... uriVars)
            throws RestClientException {

        AsyncRequestCallback callback = httpEntityCallback(request);
        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars);
        return adaptToLocationHeader(future);
    }

    @Override
    public ListenableFuture<URI> postForLocation(String url, @Nullable HttpEntity<?> request, Map<String, ?> uriVars)
            throws RestClientException {

        AsyncRequestCallback callback = httpEntityCallback(request);
        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor, uriVars);
        return adaptToLocationHeader(future);
    }

    @Override
    public ListenableFuture<URI> postForLocation(URI url, @Nullable HttpEntity<?> request)
            throws RestClientException {

        AsyncRequestCallback callback = httpEntityCallback(request);
        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.POST, callback, extractor);
        return adaptToLocationHeader(future);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> postForEntity(String url, @Nullable HttpEntity<?> request,
                                                                 Class<T> responseType, Object... uriVariables) throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> postForEntity(String url, @Nullable HttpEntity<?> request,
                                                                 Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
    }


    // PUT

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> postForEntity(URI url,
                                                                 @Nullable HttpEntity<?> request, Class<T> responseType) throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, HttpMethod.POST, requestCallback, responseExtractor);
    }

    @Override
    public ListenableFuture<?> put(String url, @Nullable HttpEntity<?> request, Object... uriVars)
            throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(request);
        return execute(url, HttpMethod.PUT, requestCallback, null, uriVars);
    }

    @Override
    public ListenableFuture<?> put(String url, @Nullable HttpEntity<?> request, Map<String, ?> uriVars)
            throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(request);
        return execute(url, HttpMethod.PUT, requestCallback, null, uriVars);
    }


    // DELETE

    @Override
    public ListenableFuture<?> put(URI url, @Nullable HttpEntity<?> request) throws RestClientException {
        AsyncRequestCallback requestCallback = httpEntityCallback(request);
        return execute(url, HttpMethod.PUT, requestCallback, null);
    }

    @Override
    public ListenableFuture<?> delete(String url, Object... uriVariables) throws RestClientException {
        return execute(url, HttpMethod.DELETE, null, null, uriVariables);
    }

    @Override
    public ListenableFuture<?> delete(String url, Map<String, ?> uriVariables) throws RestClientException {
        return execute(url, HttpMethod.DELETE, null, null, uriVariables);
    }


    // OPTIONS

    @Override
    public ListenableFuture<?> delete(URI url) throws RestClientException {
        return execute(url, HttpMethod.DELETE, null, null);
    }

    @Override
    public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Object... uriVars)
            throws RestClientException {

        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars);
        return adaptToAllowHeader(future);
    }

    @Override
    public ListenableFuture<Set<HttpMethod>> optionsForAllow(String url, Map<String, ?> uriVars)
            throws RestClientException {

        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor, uriVars);
        return adaptToAllowHeader(future);
    }

    @Override
    public ListenableFuture<Set<HttpMethod>> optionsForAllow(URI url) throws RestClientException {
        ResponseExtractor<HttpHeaders> extractor = headersExtractor();
        ListenableFuture<HttpHeaders> future = execute(url, HttpMethod.OPTIONS, null, extractor);
        return adaptToAllowHeader(future);
    }

    // exchange

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
            throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, method, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables)
            throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, method, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(URI url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException {

        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
        return execute(url, method, requestCallback, responseExtractor);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
                                                            Object... uriVariables) throws RestClientException {

        Type type = responseType.getType();
        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(type);
        return execute(url, method, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(String url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
                                                            Map<String, ?> uriVariables) throws RestClientException {

        Type type = responseType.getType();
        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(type);
        return execute(url, method, requestCallback, responseExtractor, uriVariables);
    }

    @Override
    public <T> ListenableFuture<ResponseEntity<T>> exchange(URI url, HttpMethod method,
                                                            @Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType)
            throws RestClientException {

        Type type = responseType.getType();
        AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(type);
        return execute(url, method, requestCallback, responseExtractor);
    }


    // general execution

    @Override
    public <T> ListenableFuture<T> execute(String url, HttpMethod method, @Nullable AsyncRequestCallback requestCallback,
                                           @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

        URI expanded = getUriTemplateHandler().expand(url, uriVariables);
        return doExecute(expanded, method, requestCallback, responseExtractor);
    }

    @Override
    public <T> ListenableFuture<T> execute(String url, HttpMethod method,
                                           @Nullable AsyncRequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor,
                                           Map<String, ?> uriVariables) throws RestClientException {

        URI expanded = getUriTemplateHandler().expand(url, uriVariables);
        return doExecute(expanded, method, requestCallback, responseExtractor);
    }

    @Override
    public <T> ListenableFuture<T> execute(URI url, HttpMethod method,
                                           @Nullable AsyncRequestCallback requestCallback,
                                           @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

        return doExecute(url, method, requestCallback, responseExtractor);
    }

    /**
     * Execute the given method on the provided URI. The
     * {@link org.springframework.http.client.ClientHttpRequest}
     * is processed using the {@link RequestCallback}; the response with
     * the {@link ResponseExtractor}.
     *
     * @param url               the fully-expanded URL to connect to
     * @param method            the HTTP method to execute (GET, POST, etc.)
     * @param requestCallback   object that prepares the request (can be {@code null})
     * @param responseExtractor object that extracts the return value from the response (can
     *                          be {@code null})
     * @return an arbitrary object, as returned by the {@link ResponseExtractor}
     */
    protected <T> ListenableFuture<T> doExecute(URI url, HttpMethod method,
                                                @Nullable AsyncRequestCallback requestCallback,
                                                @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

        Assert.notNull(url, "'url' must not be null");
        Assert.notNull(method, "'method' must not be null");
        try {
            org.springframework.http.client.AsyncClientHttpRequest request = createAsyncRequest(url, method);
            if (requestCallback != null) {
                requestCallback.doWithRequest(request);
            }
            ListenableFuture<ClientHttpResponse> responseFuture = request.executeAsync();
            return new ResponseExtractorFuture<>(method, url, responseFuture, responseExtractor);
        } catch (IOException ex) {
            throw new ResourceAccessException("I/O error on " + method.name() +
                    " request for \"" + url + "\":" + ex.getMessage(), ex);
        }
    }

    private void logResponseStatus(HttpMethod method, URI url, ClientHttpResponse response) {
        if (logger.isDebugEnabled()) {
            try {
                logger.debug("Async " + method.name() + " request for \"" + url + "\" resulted in " +
                        response.getRawStatusCode() + " (" + response.getStatusText() + ")");
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse response) throws IOException {
        if (logger.isWarnEnabled()) {
            try {
                logger.warn("Async " + method.name() + " request for \"" + url + "\" resulted in " +
                        response.getRawStatusCode() + " (" + response.getStatusText() + "); invoking error handler");
            } catch (IOException ex) {
                // ignore
            }
        }
        getErrorHandler().handleError(url, method, response);
    }

    /**
     * Returns a request callback implementation that prepares the request {@code Accept}
     * headers based on the given response type and configured {@linkplain
     * #getMessageConverters() message converters}.
     */
    protected <T> AsyncRequestCallback acceptHeaderRequestCallback(Class<T> responseType) {
        return new AsyncRequestCallbackAdapter(this.syncTemplate.acceptHeaderRequestCallback(responseType));
    }

    /**
     * Returns a request callback implementation that writes the given object to the
     * request stream.
     */
    protected <T> AsyncRequestCallback httpEntityCallback(@Nullable HttpEntity<T> requestBody) {
        return new AsyncRequestCallbackAdapter(this.syncTemplate.httpEntityCallback(requestBody));
    }

    /**
     * Returns a request callback implementation that writes the given object to the
     * request stream.
     */
    protected <T> AsyncRequestCallback httpEntityCallback(@Nullable HttpEntity<T> request, Type responseType) {
        return new AsyncRequestCallbackAdapter(this.syncTemplate.httpEntityCallback(request, responseType));
    }

    /**
     * Returns a response extractor for {@link ResponseEntity}.
     */
    protected <T> ResponseExtractor<ResponseEntity<T>> responseEntityExtractor(Type responseType) {
        return this.syncTemplate.responseEntityExtractor(responseType);
    }

    /**
     * Returns a response extractor for {@link HttpHeaders}.
     */
    protected ResponseExtractor<HttpHeaders> headersExtractor() {
        return this.syncTemplate.headersExtractor();
    }

    /**
     * Adapts a {@link RequestCallback} to the {@link AsyncRequestCallback} interface.
     */
    private static class AsyncRequestCallbackAdapter implements AsyncRequestCallback {

        private final RequestCallback adaptee;

        /**
         * Create a new {@code AsyncRequestCallbackAdapter} from the given
         * {@link RequestCallback}.
         *
         * @param requestCallback the callback to base this adapter on
         */
        public AsyncRequestCallbackAdapter(RequestCallback requestCallback) {
            this.adaptee = requestCallback;
        }

        @Override
        public void doWithRequest(final org.springframework.http.client.AsyncClientHttpRequest request)
                throws IOException {

            this.adaptee.doWithRequest(new ClientHttpRequest() {
                @Override
                public ClientHttpResponse execute() throws IOException {
                    throw new UnsupportedOperationException("execute not supported");
                }

                @Override
                public OutputStream getBody() throws IOException {
                    return request.getBody();
                }

                @Override
                @Nullable
                public HttpMethod getMethod() {
                    return request.getMethod();
                }

                @Override
                public String getMethodValue() {
                    return request.getMethodValue();
                }

                @Override
                public URI getURI() {
                    return request.getURI();
                }

                @Override
                public HttpHeaders getHeaders() {
                    return request.getHeaders();
                }
            });
        }
    }

    /**
     * Future returned from
     * {@link #doExecute(URI, HttpMethod, AsyncRequestCallback, ResponseExtractor)}.
     */
    private class ResponseExtractorFuture<T> extends ListenableFutureAdapter<T, ClientHttpResponse> {

        private final HttpMethod method;

        private final URI url;

        @Nullable
        private final ResponseExtractor<T> responseExtractor;

        public ResponseExtractorFuture(HttpMethod method, URI url,
                                       ListenableFuture<ClientHttpResponse> clientHttpResponseFuture,
                                       @Nullable ResponseExtractor<T> responseExtractor) {

            super(clientHttpResponseFuture);
            this.method = method;
            this.url = url;
            this.responseExtractor = responseExtractor;
        }

        @Override
        @Nullable
        protected final T adapt(ClientHttpResponse response) throws ExecutionException {
            try {
                if (!getErrorHandler().hasError(response)) {
                    logResponseStatus(this.method, this.url, response);
                } else {
                    handleResponseError(this.method, this.url, response);
                }
                return convertResponse(response);
            } catch (Throwable ex) {
                throw new ExecutionException(ex);
            } finally {
                response.close();
            }
        }

        @Nullable
        protected T convertResponse(ClientHttpResponse response) throws IOException {
            return (this.responseExtractor != null ? this.responseExtractor.extractData(response) : null);
        }
    }

}
